<?php
/**
 * coreDebug
 *
 * <code>
 * <?php
 * // 디버그 시작
 * coreDebug::init();
 *
 * // 실제 동작코드들...
 *
 * ...
 * ...
 *
 * // shutdown 핸들러에서 디버깅 정보를 출력
 * coreDebug::show();
 * ?>
 * </code>
 *
 * @package Debug
 * @author 백충덕 <cdbaek@simplexi.com>
 * @version 1.0
 * */
final class coreDebug
{
    /**
     * 디버그 모드아닌 상태 = NO DEBUG
     * */
    const DEBUG_OFF = '0';
    /**
     * $_GET의 키이름 = $_GET['debug']
     * */
    const DEBUG_KEY_NAME = 'debug';
    /**
     * $_GET의 키이름 = $_GET['d']
     * */
    const DEBUG_KEY_NAME_SHORT = 'd';
    /**
     * 디버그 모드를 지정할 쿠키이름
     * */
    const DEBUG_COOKIE_NAME = 'debug_always';
    /**
     * php.ini 의 에러를 표시할 지 여부를 나타내는 옵션이름
     * */
    const INI_DISPLAY = 'display_errors';

    /**
     * 디버그 모드 정보
     * @var bool
     * @static
     * */
    static private $bIsNeedDebug = null;
    /**
     * 시작시간 저장
     * @var float
     * @static
     * */
    static private $fStartTime = 0.0;
    /**
     * 디버그 모드 저장
     * @var string
     * @static
     * */
    static private $sDebugMode = '0';
    /**
     * 디버그 모드 이름 배열. controller 이름과 대응.
     * @var array
     * @static
     * */
    static private $aDebugMode = array(
        '1' => 'Web',
        '2' => 'Db',
        '3' => 'Console',
        '4' => 'Msg',
        '5' => 'Firebug'
    );
    /**
     * 디버그 컨트롤러
     * @var array
     * @static
     * */
    static private $aDebugController = array();
    /**
     * 디버그 후킹 모드 이름 배열. controller 이름과 대응.
     * @var array
     * @static
     */
    static private $aDebugHookMode = array(
        'phpinfo',
        'config',
        'setcookie',
        'history'
    );
    /**
     * 디버그 가능상태인지 나타내는 변수
     * @var bool
     * @static
     * */
    static private $bPossibleDebug = null;

    /**
     * constructor
     * */
    private function __construct()
    {
    }

    /**
     * 디버그 모드 세팅 (0 = no debug)
     * @param string $sDebugMode 세팅할 디버그 모드
     * @static
     * */
    static public function setDebugMode($sDebugMode = '')
    {
        // 값이 없으면 Web으로 디버깅하는 것으로 간주함
        if ($sDebugMode==='') $sDebugMode='1';

        self::$sDebugMode = $sDebugMode;
    }

    /**
     * 디버깅 초기화
     * @static
     * */
    static public function init()
    {
        // 데이터 초기화
        self::$bIsNeedDebug = null;

        // 디버그 모드 세팅
        self::setDebugMode(self::getDebugModeInput());

        $bIsNeedDebug = self::isNeedDebug();

        //
        // !!WARNING!!
        //
        // 디버그 사용 가능여부 상수 설정
        //
        define('FW_DEBUG_ENABLED', $bIsNeedDebug);

        // Hook (config 모드)
        if ($bIsNeedDebug===true) self::debugHook();

        // ini 세팅변경
        self::setIni($bIsNeedDebug);

        // 에러핸들러 세팅
        if ($bIsNeedDebug===true) {
            $coreDebugError = new coreDebugError;
            set_error_handler(array($coreDebugError, 'errorHandler'), ini_get('error_reporting'));
            set_exception_handler('_exceph');
            if (function_exists('xdebug_time_index')===false) self::$fStartTime = microtime(true);
        }
    }

    /**
     * 디버그 모드와 매칭되는 controller 호출
     * coreDebugControllerDEBUG_MODE_NAME::show();
     * @static
     * */
    static public function show()
    {
        if (self::isNeedDebug()===false) return false;

        $sController = self::getController();

        self::execController($sController);
    }

    /**
     * 디버그 모드이름 배열 반환
     * @return array 디버그 모드이름 배열 반환.
     * @static
     * */
    static public function getDebugModeArr()
    {
        return self::$aDebugMode;
    }

    /**
     * 시작 시간 반환.
     * xdebug_time_index() 함수가 없을 경우 이 값이 필요함.
     * @return float 저장된 시작 시간 반환.
     * @static
     * */
    static public function getStartTime()
    {
        return self::$fStartTime;
    }

    /**
     * 디버그 모드, 권한을 검사하여 디버그 여부 결정
     * @param string $sCookieName 디버그 여부를 체크할 항목의 쿠키명
     * @return bool 디버그가 필요한 상태면 true, 아니면 false 반환.
     * @static
     * */
    static public function isNeedDebug($sCookieName='')
    {
        // 전역 디버그 모드가 false 라면 무조건 false 반환
        if (self::$bIsNeedDebug === false) return false;

        // 전역 디버그 모드 검사
        if ($sCookieName=='') {
            if (self::$bIsNeedDebug===null) {
                $sDebug = self::$sDebugMode;
                if ($sDebug==self::DEBUG_OFF) self::$bIsNeedDebug = false;

                if (self::$bIsNeedDebug!==false && self::isPossibleDebug()===false) self::$bIsNeedDebug = false;
            }

            // 전역 디버그 모드가 아니라면 하위 검사는 필요없음
            if (self::$bIsNeedDebug === false) return false;
        }

        // 쿠키검사 - 영역별 디버그 모드 검사
        if (strlen($sCookieName)>0) return coreDebugConfig::isUse($sCookieName);

        return true;
    }

    /**
     * 디버그 권한이 있는지 체크
     * @return bool 디버그 권한이 있으면 true, 없으면 false 반환
     * @static
     * */
    static public function isPossibleDebug()
    {
        if (self::$bPossibleDebug === null) {
            self::$bPossibleDebug = T_debugPossible();
        }

        return self::$bPossibleDebug;
    }

    /**
     * 디버그 데이터를 수집하기 전 hook (config 등)
     * @static
     */
    static private function debugHook()
    {
        if (self::isHookMode() === true) {
            $sHookControllerName = self::getControllerName(self::$sDebugMode);
            self::execController($sHookControllerName);
        }
    }

    /**
     * hook 모드인지 정상적인 디버그 데이터 수집모드인지 체크
     * @return bool hook모드이면 true, 아니면 false 반환
     * @static
     */
    static private function isHookMode()
    {
        return in_array(self::$sDebugMode, self::$aDebugHookMode);
    }

    /**
     * 디버그 모드에 따른 ini 세팅
     * @param bool $bIsNeedDebug true 이면 에러표시, false이면 표시하지 않음.
     * @static
     * */
    static private function setIni($bIsNeedDebug)
    {
        ini_set(self::INI_DISPLAY, $bIsNeedDebug);

        if ($bIsNeedDebug==true) {
            ini_set('xdebug.var_display_max_children', 9999);
            ini_set('xdebug.var_display_max_data', 9999999);
            ini_set('xdebug.var_display_max_depth', 999);
        }
    }

    /**
     * $_GET, $_COOKIE 값을 통해 디버그 모드 추출
     * @return integer 디버그 모드라면 0 보다 큰 값을 반환. 아니라면 0 반환.
     * @static
     * */
    static private function getDebugModeInput()
    {
        // 디버그할 페이지가 아니면 DEBUG_OFF
        if (self::isDebugUrl() === false) return self::DEBUG_OFF;

        // 디버그 모드를 의미하는 $_GET값이 없음
        if ((isset($_GET[self::DEBUG_KEY_NAME])===false) && (isset($_GET[self::DEBUG_KEY_NAME_SHORT])===false)) {
            // 쿠키검사 - 쿠키값이 true로 세팅되어 있으면 웹 디버그모드로 간주함
            if (coreDebugConfig::isTrue('always') == true) return '';

            // 디버그 모드를 의미하는 $_GET, $_COOKIE 모두 없음
            return self::DEBUG_OFF;
        }

        // 간단히 d 라고 입력하면 Web 디버그 모드로 간주함.
        if (isset($_GET[self::DEBUG_KEY_NAME_SHORT])) return $_GET[self::DEBUG_KEY_NAME_SHORT];
        else if (isset($_GET[self::DEBUG_KEY_NAME])) return $_GET[self::DEBUG_KEY_NAME];
    }

    /**
     * URL을 체크하여 디버그할 페이지인지 여부 확인
     * @return bool 디버그할 페이지이면 true, 아니면 false 반환
     * @static
     * */
    static private function isDebugUrl()
    {
        $aUrl = parse_url($_SERVER['REQUEST_URI']);
        $sPath = trim($aUrl['path']);

        // 디버그 제외 URL 체크
        $aExceptDebug = coreDebugConfig::getConfig('except_debug_url');
        if (is_array($aExceptDebug)==true && count($aExceptDebug)>0) {
            foreach ($aExceptDebug as $v) {
                if (strpos($sPath, $v)!==false) return false;
            }
        }

        // 퍼머링크 호출 체크 (예 = /exec/front/module/action/?no=1)
        if (substr($sPath, -1) == '/') return true;

        $aPathInfo = pathinfo($sPath);

        // 퍼머링크인데 마지막 문자열이 / 이 아닐 경우 (예 = /disp/admin/intro/preferences)
        if (array_key_exists('extension', $aPathInfo)==false || strlen(trim($aPathInfo['extension']))<1) return true;

        $aAllowedExtension = array('html', 'htm', 'xml', 'php');
        return in_array($aPathInfo['extension'], $aAllowedExtension);
    }

    /**
     * 디버그 모드에 맞는 controller 이름 반환. contDebugDEBUG_MODE_NAME
     * @return string contDebugDEBUG_MODE_NAME
     * @static
     * */
    static private function getController()
    {
        $sControllerName = '';
        if (array_key_exists(self::$sDebugMode, self::$aDebugMode)===true) {
            $sControllerName = self::$aDebugMode[self::$sDebugMode];
            if (strlen($sControllerName)>0) {
                $sControllerName = self::getControllerName($sControllerName);
            }
        }

        return $sControllerName;
    }

    /**
     * 디버그 컨트롤러명 반환
     *
     * @param string $sSuffix 디버그 컨트롤러명 suffix
     * @return string
     * @static
     */
    static private function getControllerName($sSuffix)
    {
        $sController = 'coreDebugController'.ucfirst(strtolower($sSuffix));

        return $sController;
    }

    /**
     * 선택된 디버그 컨트롤러 실행
     *
     * @param string $sController 클래스명(coreDebugController[NAME])
     * @return boolean
     * @static
     */
    static private function execController($sController)
    {
        if (array_key_exists($sController, self::$aDebugController) == false) {
            if (!class_exists($sController)) {
                throw new Exception("There's no Controller class named ".$sController);
            }

            self::$aDebugController[$sController] = new $sController();
        }

        if (is_object(self::$aDebugController[$sController]) == false) throw new Exception("There's no Controller object named ".$sController);

        self::$aDebugController[$sController]->show();
    }
}
